﻿using Microsoft.Speech.Recognition;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows.Forms;
using XbmcManager;
using XbmcManager.Types;

namespace Imperium
{
    class ImperiumController
    {
        private const float confidenceMinNotName = 0.55f;
        private const float confidenceMinName = 0.7f;

        private enum state { Idle, Ready };
        private state currentState;
        private XbmcController xbmc = new XbmcController(
            Properties.Settings.Default.IpAddress,
            Properties.Settings.Default.Port,
            Properties.Settings.Default.Username,
            Properties.Settings.Default.Password);

        private ImperiumModel model;
        private KinectSpeechEngine engine;
        private Kinect kinect = new Kinect();
        private Timer timer = new Timer();

        private Grammar commands;
        private List<XbmcTVShow> tvShows;
        private List<XbmcMovie> movies;
        private bool scanHappened = false;

        /// <summary>
        /// Constructor
        /// </summary>
        public ImperiumController(ImperiumModel m)
        {
            this.model = m;

            this.currentState = state.Idle;

            SetTimer();

            SetupKinect();

            this.model.XbmcState = TestConnection();
            
            PerformCaching();
        }

        private void SetTimer()
        {
            this.timer.Tick += new EventHandler(TimerCompleted);
            this.timer.Interval = Properties.Settings.Default.IdleTime;
        }

        public void SetupKinect()
        {
            try
            {
                this.kinect.Connect();

                if (this.kinect.Sensor != null)
                {
                    this.engine = new KinectSpeechEngine(this.model,
                        this.kinect, 
                        GetNameCommand());

                    this.model.KinectState = "Success";
                }
                else
                {
                    this.model.KinectState = Properties.Resources.NoKinectReady;
                    System.Diagnostics.Debug.WriteLine("Failed to start speech engine.");
                }
            }
            catch
            {
                System.Diagnostics.Debug.WriteLine("Failed to connect to sensor.");
                this.model.KinectState = Properties.Resources.NoKinectReady;
            }
        }
        
        private void PerformCaching()
        {
            CacheTVShows();
            CacheMovies();
            CacheCommands();
        }

        private void CacheTVShows()
        {
            this.tvShows = this.xbmc.VideoLibrary.GetTVShows();
        }

        private void CacheMovies()
        {
            this.movies = this.xbmc.VideoLibrary.GetMovies();
        }

        private void CacheCommands()
        {
            SetCommands();
        }

        /// <summary>
        /// Create list of commands that are listened for.
        /// </summary>
        private void SetCommands()
        {
            Choices cmds = new Choices();
            // Regular Commands
            cmds.Add("FULLSCREEN");
            cmds.Add("PLAY");
            cmds.Add("PAUSE");
            cmds.Add("STOP");
            cmds.Add("MUTE");
            cmds.Add("UNMUTE");
            cmds.Add("HOME");
            cmds.Add("BACK");
            cmds.Add("SELECT");
            cmds.Add("LEFT");
            cmds.Add("RIGHT");
            cmds.Add("UP");
            cmds.Add("DOWN");
            cmds.Add("VOLUME FIFTY");
            cmds.Add("VOLUME SIXTY");
            cmds.Add("VOLUME SEVENTY");
            cmds.Add("VOLUME EIGHTY");
            cmds.Add("VOLUME NINETY");
            cmds.Add("VOLUME ONE HUNDRED");
            cmds.Add("START");
            cmds.Add("QUIT");
            cmds.Add("SCAN");
            cmds.Add("NEXT CHAPTER");
            cmds.Add("PREVIOUS CHAPTER");
            cmds.Add("START");

            // Add each movie title to command list; "Play [MOVIE NAME]"
            foreach (XbmcMovie movie in movies)
            {
                cmds.Add("Play " + movie.Title);
            }

            // Add each TV Series to command list; "Play [TV Series]"
            foreach (XbmcTVShow tvshow in tvShows)
            {
                cmds.Add("Play " + tvshow.Title);
                cmds.Add("Play " + tvshow.Title + " Random");
            }

            GrammarBuilder gb = new GrammarBuilder();
            gb.Append(cmds);

            this.commands = new Grammar(gb);
        }

        /// <summary>
        /// Determine how the app functions in each state of the program.
        /// Including pre and post steps for each state.
        /// </summary>
        private void ChangeState(state oldState, state newState)
        {
            Debug.Print("Transitioning State from " + oldState + " to " + newState + ".");
            this.currentState = newState;

            if (GoingIdleState(oldState, newState))
            {
                this.xbmc.SoftMuter.UnMute();
                
                // Set Grammer to only contain Name
                this.engine.LoadGrammar(GetNameCommand());
                this.model.SetImperiumLogo(false);
                
                this.timer.Stop();
            }

            if (GettingReadyState(oldState, newState))
            {
                this.xbmc.SoftMuter.SoftMute();
                this.xbmc.GUI.Notification("Imperium", "Ready for command");
                
                // Load Grammar
                this.engine.LoadGrammar(commands);
                this.model.SetImperiumLogo(true);
            }

            if (newState != state.Idle)
            {
                this.timer.Stop();
                this.timer.Start();
            }
        }

        private bool GoingIdleState(state oldState, state newState)
        {
            if (oldState != state.Idle && newState == state.Idle)
            {
                return true;
            }
            return false;
        }

        private bool GettingReadyState(state oldState, state newState)
        {
            if (oldState != state.Ready && newState == state.Ready)
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Determine which type of command was given.
        /// </summary>
        public void CommandGiven(String command, float confidence)
        {
            // If we are idle and we hear the name
            if (NameRecognized(confidence))
            {
                // If a scan has happened since last time we cached we need to update our caches.
                if (this.scanHappened)
                {
                    PerformCaching();
                    this.scanHappened = false;
                }
                ChangeState(this.currentState, state.Ready);
            }
            // If we are in the ready state and receive command
            else if (CommandRecognized(confidence))
            {
                DisplayCommand(command);
                
                // Look for TV Show or Movie..
                if (command.StartsWith("PLAY "))
                {
                    string title = ParseTitle(command);

                    // Search through TV shows first
                    var tvShowQuery = from t in tvShows
                                      where t.Title.ToUpper().Contains(title)
                                      select t;

                    if (tvShowQuery.Any())
                    {
                        var tvShow = tvShowQuery.First();
                        ExecuteTVCommand(tvShow, command);
                    }
                    else
                    {
                        ExecuteMovieCommand(title);
                    }

                    // Reset state immediately after command is recognized 
                    // instead of waiting for timer to reset it.
                    ChangeState(this.currentState, state.Idle);
                }
                else
                {
                    ExecuteXbmcCommand(command);
                }
            }
        }

        private bool NameRecognized(float confidence)
        {
            return this.currentState == state.Idle && confidence > confidenceMinName;
        }

        private bool CommandRecognized(float confidence)
        {
            return this.currentState == state.Ready && confidence > confidenceMinNotName;
        }

        private void DisplayCommand(String command)
        {
            this.xbmc.GUI.Notification("Command Recognized", string.Format("{0}{1}",
                char.ToUpper(command[0]),
                command.Substring(1)));
        }

        private static string ParseTitle(String command)
        {
            string title = command.Replace("PLAY ", "");
            title = title.Replace(" RANDOM", "");
            return title;
        }

        /// <summary>
        /// Play TV Show specified.
        /// </summary>
        void ExecuteTVCommand(XbmcTVShow tvShow, String command)
        {
            int id = tvShow.ID;

            if (command.Contains(" RANDOM"))
            {
                // Play random episode when "Play [Tv Show] Random" is recognized.
                XbmcEpisode episode = this.xbmc.VideoLibrary.GetRandomEpisodeByTVShow(id);
                this.xbmc.Player.OpenFile(episode.File);
            }
            else
            {
                // Play Next Unwatched episode
                XbmcEpisode episode = this.xbmc.VideoLibrary.GetNextUnwatchedEpisodeByTVShow(id);
                if (episode == null)
                {
                    this.xbmc.GUI.Notification("No unwatched episodes of ", tvShow.Title);
                }
                else
                {
                    this.xbmc.Player.OpenFile(episode.File);
                }
            }
        }

        /// <summary>
        /// Play Movie specified.
        /// </summary>
        void ExecuteMovieCommand(String movieTitle)
        {
            var movieQuery = from m in movies
                             where m.Title.ToUpper().Contains(movieTitle)
                             select m;

            if (movieQuery.Any())
            {
                var file = movieQuery.First().File;
                this.xbmc.Player.OpenFile(file);
            }
        }

        /// <summary>
        /// Send JSON Command
        /// </summary>
        void ExecuteXbmcCommand(String command)
        {
            switch (command)
            {
                case "PLAY":
                    this.xbmc.Player.Play();
                    ChangeState(currentState, state.Idle);
                    break;
                case "PAUSE":
                    this.xbmc.Player.Pause();
                    ChangeState(currentState, state.Idle);
                    break;
                case "STOP":
                    this.xbmc.Player.Stop();
                    ChangeState(currentState, state.Idle);
                    break;
                case "NEXT CHAPTER":
                    this.xbmc.Player.NextChapter();
                    ChangeState(currentState, state.Idle);
                    break;
                case "PREVIOUS CHAPTER":
                    this.xbmc.Player.PreviousChapter();
                    ChangeState(currentState, state.Idle);
                    break;
                case "MUTE":
                    this.xbmc.Application.Mute();
                    ChangeState(currentState, state.Idle);
                    break;
                case "UNMUTE":
                    this.xbmc.Application.UnMute();
                    ChangeState(currentState, state.Idle);
                    break;
                case "HOME":
                    this.xbmc.NavHome();
                    ChangeState(currentState, state.Idle);
                    break;
                case "BACK":
                    this.xbmc.NavBack();
                    ChangeState(currentState, state.Idle);
                    break;
                case "SELECT":
                    this.xbmc.Select();
                    ChangeState(currentState, state.Idle);
                    break;
                case "LEFT":
                    this.xbmc.NavLeft();
                    // Set state to ready again to string multiple commands together like "UP, UP, LEFT" to navigate menu.
                    ChangeState(currentState, state.Ready);
                    break;
                case "RIGHT":
                    this.xbmc.NavRight();
                    ChangeState(currentState, state.Ready);
                    break;
                case "UP":
                    this.xbmc.NavUp();
                    ChangeState(currentState, state.Ready);
                    break;
                case "DOWN":
                    this.xbmc.NavDown();
                    ChangeState(currentState, state.Ready);
                    break;
                case "START":
                    this.xbmc.Launcher.Start();
                    ChangeState(currentState, state.Idle);
                    break;
                case "QUIT":
                    this.xbmc.Application.Quit();
                    ChangeState(currentState, state.Idle);
                    break;
                case "SCAN":
                    this.xbmc.VideoLibrary.Scan();
                    // Reload movies and tv shows after scanning.
                    // We can't recache right after because the xbmc.scan happens asynchronously.
                    scanHappened = true;
                    ChangeState(currentState, state.Idle);
                    break;
                case "VOLUME FIFTY":
                    this.xbmc.Application.Volume(50);
                    ChangeState(currentState, state.Idle);
                    break;
                case "VOLUME SIXTY":
                    this.xbmc.Application.Volume(60);
                    ChangeState(currentState, state.Idle);
                    break;
                case "VOLUME SEVENTY":
                    this.xbmc.Application.Volume(70);
                    ChangeState(currentState, state.Idle);
                    break;
                case "VOLUME EIGHTY":
                    this.xbmc.Application.Volume(80);
                    ChangeState(currentState, state.Idle);
                    break;
                case "VOLUME NINETY":
                    this.xbmc.Application.Volume(90);
                    ChangeState(currentState, state.Idle);
                    break;
                case "VOLUME ONE HUNDRED":
                    this.xbmc.Application.Volume(100);
                    ChangeState(currentState, state.Idle);
                    break;
                case "FULLSCREEN":
                    this.xbmc.GUI.FullScreen();
                    ChangeState(currentState, state.Idle);
                    break;
            }
        }

        /// <summary>
        /// Create Command Name to listen for.
        /// </summary>
        public Grammar GetNameCommand()
        {
            GrammarBuilder gb = new GrammarBuilder(Properties.Settings.Default.Name.ToUpper());
            Grammar g = new Grammar(gb);

            return g;
        }

        /// <summary>
        /// What happens when we don't hear a second command after the Keyword command.
        /// </summary>
        void TimerCompleted(object sender, EventArgs e)
        {
            Debug.Print("Timer Expired");
            ChangeState(currentState, state.Idle);
        }

        /// <summary>
        /// Tests the connection with XBMC with the current settings.
        /// </summary>
        /// <returns>A string describing either a success or error message.</returns>
        public static String TestConnection()
        {
            String output = "";
            const String message = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetActivePlayers\",\"id\":1}";

            try
            {
                HttpWebRequest httpWebRequest = CreateRequest();

                WriteJsonString(message, httpWebRequest);

                //Gets the response.
                HttpWebResponse httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
                using (StreamReader streamReader = new StreamReader(httpResponse.GetResponseStream()))
                {
                    string result = streamReader.ReadToEnd();
                }
                output = Properties.Resources.NetConnectionSuccess;
            }
            catch (WebException ex)
            {
                Console.WriteLine(ex.Status);
                if (ex.Status == WebExceptionStatus.ProtocolError)
                {
                    var response = ex.Response as HttpWebResponse;
                    if (response != null)
                    {
                        if (response.StatusCode.Equals(HttpStatusCode.Unauthorized))
                            output = Properties.Resources.NetErrorUserPw;
                        else
                            output = Properties.Resources.NetErrorHttpCode + response.StatusCode;
                    }
                    else
                    {
                        output = Properties.Resources.NetErrorGeneric;
                    }
                }
                else
                {
                    // no http status code available
                    Process[] processes = Process.GetProcessesByName("XBMC");
                    if (processes.Length == 0)
                        output = Properties.Resources.NetErrorNoXbmc;
                    else
                        output = Properties.Resources.NetErrorConnectionFailed;
                }
            }
            catch (UriFormatException)
            {
                output = Properties.Resources.NetErrorMalformedURI;
            }

            return output;
        }

        private static HttpWebRequest CreateRequest()
        {
            String url = "http://" + Properties.Settings.Default.IpAddress + ":" + Properties.Settings.Default.Port + "/jsonrpc";
            HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
            NetworkCredential networkCredential = new NetworkCredential(Properties.Settings.Default.Username, Properties.Settings.Default.Password);
            httpWebRequest.Credentials = networkCredential;
            httpWebRequest.ContentType = "application/json";
            httpWebRequest.Method = "POST";
            return httpWebRequest;
        }

        private static void WriteJsonString(String message, HttpWebRequest httpWebRequest)
        {
            using (StreamWriter streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
            {
                streamWriter.Write(message);
            }
        }
    }
}
